package demo.google.common.util.concurrent;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.util.concurrent.RateLimiter;

/**
 * <h1>令牌桶算法</h1>令牌桶会以一个恒定的速率向固定容量大小桶中放入令牌，当有流量来时则取走一个或多个令牌。当桶中没有令牌则将当前请求丢弃或阻塞。
 * 
 * <h1>RateLimiter类</h1>经常用于限制对一些物理资源或者逻辑资源的访问速率。与Semaphore 相比，Semaphore
 * 限制了并发访问的数量而不是使用速率。
 * 通过设置许可证的速率来定义RateLimiter。在默认配置下，许可证会在固定的速率下被分配，速率单位是每秒多少个许可证。
 * 为了确保维护配置的速率，许可会被平稳地分配，许可之间的延迟会做调整。 可能存在配置一个拥有预热期的RateLimiter
 * 的情况，在这段时间内，每秒分配的许可数会稳定地增长直到达到稳定的速率。
 * 
 * <h1>原理</h1> RateLimiter使用的是一种叫令牌桶的流控算法，RateLimiter会按照一定的频率往桶里扔令牌，线程拿到令牌才能执行，
 * 比如你希望自己的应用程序QPS不要超过1000，那么RateLimiter设置1000的速率后，就会每秒往桶里扔1000个令牌。
 * 有一点很重要，那就是请求的许可数从来不会影响到请求本身的限制（调用acquire(1) 和调用acquire(1000)
 * 将得到相同的限制效果，如果存在这样的调用的话），但会影响下一次请求的限制，
 * 也就是说，如果一个高开销的任务抵达一个空闲的RateLimiter，它会被马上许可，但是下一个请求会经历额外的限制，从而来偿付高开销任务。
 * 注意：RateLimiter 并不提供公平性的保证。
 * 
 * <h1>抢购DEMO</h1>
 * 当然了，真正的抢购不是这么简单，瞬间的流量洪峰会冲垮服务器的负载，当100万人抢1万个小米时，连接口都请求不进来，更别提接口里的令牌分配了。
 * 此时就需要做上一层的限流，我们可以选择在上一层做分布式，开多个服务，先做一次限流，淘汰掉绝大多数运气不好的用户，甚至可以随机丢弃某些规则的用户，
 * 迅速拦截90%的请求，让你去网页看单机排队动画，还剩10万。
 * 10万也太大，足以冲垮数据层，那就进队列MQ，用MQ削峰后，然后才放进业务逻辑里，再进行RateLimiter的限流，
 * 此时又能拦截掉90%的不幸者，还剩1万，1万去交给业务逻辑和数据层，用redis和DB来处理库存。 恭喜，你就是那个漏网之鱼。
 * 重点在于迅速拦截掉99%的不幸者，避免让他们去接触到数据层。而且不能等待时间太长，最好是请求的瞬间就能确定你是永远看单机动画最好。
 *
 */
public class RateLimiterDemo {

    static Logger logger = LoggerFactory.getLogger(RateLimiterDemo.class);

    static ExecutorService executorService = Executors.newFixedThreadPool(16);

    /**
     * acquire 方法会阻塞
     */
    @Test
    public void acquire() {
        RateLimiter limiter = RateLimiter.create(30);
        while (true) {
            executorService.submit(() -> {
                long t0 = System.currentTimeMillis();
                double a = limiter.acquire();
                logger.info("验证一下Guava的限速器,sleep time :{},cost:{}", a * 1000, System.currentTimeMillis() - t0);
            });
        }
    }

    /**
     * tryAcquire(long timeout, TimeUnit unit)方法，
     * 指定一个超时时间，一旦判断出在timeout时间内还无法取得令牌，就返回false。
     * 注意，这里并不是真正的等待了timeout时间，而是被判断为即便过了timeout时间，也无法取得令牌。这个是不需要等待的。
     * 
     * 从RateLimiter 获取许可如果该许可可以在不超过timeout的时间内获取得到的话，
     * 或者如果无法在timeout过期之前获取得到许可的话，那么立即返回false（无需等待）
     */
    @Test
    public void tryAcquire() {
        RateLimiter limiter = RateLimiter.create(1.0);
        while (true) {
            executorService.submit(() -> {
                if (limiter.tryAcquire(1, TimeUnit.SECONDS)) {
                    logger.info("获取令牌成功");
                } else {
                    double rate = limiter.getRate();
                    limiter.setRate(rate + 0.1);
                    logger.info("获取令牌失败");
                }
            });
        }
    }

    /**
     * 根据指定的稳定吞吐率创建RateLimiter，这里的吞吐率是指每秒多少许可数（通常是指QPS，每秒多少查询）。
     * 
     * @param permitsPerSecond
     * @return
     */
    public static RateLimiter create(double permitsPerSecond) {
        return RateLimiter.create(permitsPerSecond);
    }

    /**
     * 根据指定的稳定吞吐率和预热期来创建RateLimiter，这里的吞吐率是指每秒多少许可数（通常是指QPS，每秒多少查询），在这段预热时间内，
     * RateLimiter每秒分配的许可数会平稳地增长直到预热期结束时达到其最大速率（只要存在足够请求数来使其饱和）。同样地，如果RateLimiter
     * 在warmupPeriod时间内闲置不用，它将会逐步地返回冷却状态。也就是说，它会像它第一次被创建般经历同样的预热期。返回的RateLimiter
     * 主要用于那些需要预热期的资源，这些资源实际上满足了请求（比如一个远程服务），而不是在稳定（最大）的速率下可以立即被访问的资源。返回的RateLimiter
     * 在冷却状态下启动（即预热期将会紧跟着发生），并且如果被长期闲置不用，它将回到冷却状态。
     * 
     * @param permitsPerSecond
     * @param warmupPeriod     在这段时间内RateLimiter会增加它的速率，在抵达它的稳定速率或者最大速率之前
     * @param unit             参数warmupPeriod 的时间单位
     * 
     * @return
     */
    public static RateLimiter create(double permitsPerSecond, long warmupPeriod, TimeUnit unit) {
        return RateLimiter.create(permitsPerSecond, warmupPeriod, unit);
    }
}
